package com.atguigu.gmall.product.service.impl;

import com.alibaba.fastjson.JSON;
import com.atguigu.gmall.common.constant.RedisConst;
import com.atguigu.gmall.common.redis.GmallCache;
import com.atguigu.gmall.product.mapper.SkuAttrValueMapper;
import com.atguigu.gmall.product.mapper.SkuSaleAttrValueMapper;
import com.atguigu.gmall.product.model.*;
import com.atguigu.gmall.product.service.*;
import com.atguigu.gmall.rabbit.config.MqConst;
import com.atguigu.gmall.rabbit.service.RabbitService;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;

import java.math.BigDecimal;
import java.util.*;
import java.util.concurrent.TimeUnit;

/**
 * @author: atguigu
 * @create: 2023-09-01 11:43
 */
@Slf4j
@Service
public class SkuManageServiceImpl implements SkuManageService {

    @Autowired
    private SkuInfoService skuInfoService;

    @Autowired
    private SkuImageService skuImageService;

    @Autowired
    private SkuAttrValueService skuAttrValueService;

    @Autowired
    private SkuSaleAttrValueService skuSaleAttrValueService;

    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private RedissonClient redissonClient;

    @Autowired
    private RabbitService rabbitService;

    /**
     * 保存sku商品
     * 1.将前端提交sku商品基本信息封装到SkuInfo对象，向sku_info新增一条记录
     * 2.将提交多张图片列表封装SkuImage集合中，批量向sku_image表新增多条记录
     * 3.将提交多个平台属性列表封装SkuAttrValue集合中，批量向sku_attr_value表新增多条记录
     * 4.将提交多个销售属性列表封装SkuSaleAttrValue集合中，批量向sku_sale_attr_value表新增多条记录
     *
     * @param skuInfo
     * @return
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    //使用Security安全框架提供权限验证注解
    //@HasAuthoris("goods:manage:insert")  //只有当前用户具备”goods:manage:insert“权限  才能调用该方法
    //@HasAnyRoles({"admin","manage"})//只有当前用户具有”admin"或者"manage"角色  才能调用该方法
    public void saveSkuInfo(SkuInfo skuInfo) {
        //1.将前端提交sku商品基本信息封装到SkuInfo对象，向sku_info新增一条记录
        skuInfoService.save(skuInfo);
        Long skuId = skuInfo.getId();

        //2.将提交多张图片列表封装SkuImage集合中，批量向sku_image表新增多条记录
        List<SkuImage> skuImageList = skuInfo.getSkuImageList();
        if (!CollectionUtils.isEmpty(skuImageList)) {
            //2.1 将商品图片关联SKUID
            for (SkuImage skuImage : skuImageList) {
                skuImage.setSkuId(skuId);
            }
            //2.2 批量保存
            skuImageService.saveBatch(skuImageList);
        }

        //3.将提交多个平台属性列表封装SkuAttrValue集合中，批量向sku_attr_value表新增多条记录
        List<SkuAttrValue> skuAttrValueList = skuInfo.getSkuAttrValueList();
        if (!CollectionUtils.isEmpty(skuAttrValueList)) {
            //3.1 将平台属性关联SKUID
            for (SkuAttrValue skuAttrValue : skuAttrValueList) {
                skuAttrValue.setSkuId(skuId);
            }
            //3.2 批量保存
            skuAttrValueService.saveBatch(skuAttrValueList);
        }

        //4.将提交多个销售属性列表封装SkuSaleAttrValue集合中，批量向sku_sale_attr_value表新增多条记录
        List<SkuSaleAttrValue> skuSaleAttrValueList = skuInfo.getSkuSaleAttrValueList();
        if (!CollectionUtils.isEmpty(skuSaleAttrValueList)) {
            //4.1 将销售属性关联SKUID，SPUID
            for (SkuSaleAttrValue skuSaleAttrValue : skuSaleAttrValueList) {
                skuSaleAttrValue.setSkuId(skuId);
                skuSaleAttrValue.setSpuId(skuInfo.getSpuId());
            }
            //4.2 批量保存
            skuSaleAttrValueService.saveBatch(skuSaleAttrValueList);
        }

        //todo 5.将新增后商品skuId存入布隆过滤器
        RBloomFilter<Long> bloomFilter = redissonClient.getBloomFilter(RedisConst.SKU_BLOOM_FILTER);
        bloomFilter.add(skuId);

    }

    /**
     * 分页查询商品SKU列表
     *
     * @param skuInfoPage 分页对象
     * @param category3Id 三级分类ID
     * @return
     */
    @Override
    public Page<SkuInfo> getSkuList(Page<SkuInfo> skuInfoPage, Long category3Id) {
        //1.构建条件对象
        LambdaQueryWrapper<SkuInfo> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(SkuInfo::getCategory3Id, category3Id);
        queryWrapper.select(SkuInfo::getId, SkuInfo::getSkuName, SkuInfo::getPrice, SkuInfo::getSkuDefaultImg, SkuInfo::getSpuId, SkuInfo::getSkuDesc, SkuInfo::getIsSale);
        queryWrapper.orderByDesc(SkuInfo::getId, SkuInfo::getUpdateTime);
        //2.执行分页查询
        return skuInfoService.page(skuInfoPage, queryWrapper);
    }


    /**
     * 上架商品
     *
     * @param skuId
     */
    @Override
    public void onSale(Long skuId) {
        //1.修改商品表中商品状态
        SkuInfo skuInfo = new SkuInfo(skuId);
        skuInfo.setIsSale(1);
        skuInfoService.updateById(skuInfo);

        //2.将上架商品skuId存入布隆过滤器
        RBloomFilter<Long> bloomFilter = redissonClient.getBloomFilter(RedisConst.SKU_BLOOM_FILTER);
        bloomFilter.add(skuId);

        //2.基于MQ异步消息通知搜索服务新增索引库文档
        rabbitService.sendMessage(MqConst.EXCHANGE_DIRECT_GOODS, MqConst.ROUTING_GOODS_UPPER, skuId);
    }


    /**
     * 下架商品
     *
     * @param skuId
     */
    @Override
    public void cancelSale(Long skuId) {
        //1.修改商品表中商品状态  update sku_info set is_sale=? where id = ?
        LambdaUpdateWrapper<SkuInfo> updateWrapper = new LambdaUpdateWrapper<>();
        updateWrapper.set(SkuInfo::getIsSale, 0);
        updateWrapper.eq(SkuInfo::getId, skuId);
        skuInfoService.update(updateWrapper);


        //2.基于MQ异步消息通知搜索服务删除索引库文档
        rabbitService.sendMessage(MqConst.EXCHANGE_DIRECT_GOODS, MqConst.ROUTING_GOODS_LOWER, skuId);
    }


    /**
     * 伪代码模拟实现商品修改
     * 解决缓存一致性问题,业务场景不能容忍不一致
     *
     * @param skuInfo
     */
    @Transactional(rollbackFor = Exception.class)
    public void updateSkuInfo(SkuInfo skuInfo) throws InterruptedException {
        //①解决方案：双写方案之 先更新数据库再更新缓存、或者反着，变更顺序结果一样导致并发写导致数据不一致-适用没有并发场景
        //1先更新数据库
        //skuInfoService.updateById(skuInfo);

        //2.再更新缓存
        String dataKey = RedisConst.SKUKEY_PREFIX + skuInfo.getId() + RedisConst.SKUKEY_SUFFIX;
        //redisTemplate.opsForValue().set(dataKey, skuInfo);

        //②解决方案：先删除缓存再更新数据 并发读写情况下产生数据不一致
        //redisTemplate.delete(dataKey);
        //skuInfoService.updateById(skuInfo);

        //③解决方案：延时双删-简单能够解决企业95%以上场景  问题：在数据库主从复制模式下，主从存在延迟（大于睡眠时间）产生数据不一致
        //1.先删除缓存
        redisTemplate.delete(dataKey);
        //2.更新数据库
        skuInfoService.updateById(skuInfo);
        //3.睡眠一段时间（确保此刻正在并发读线程执行完毕)经过大量测试查询操作，查库将结果放入缓存耗时最大值（稍微放大些）
        TimeUnit.MILLISECONDS.sleep(200);
        //4.再次删除缓存
        redisTemplate.delete(dataKey);
    }


    /**
     * 根据SkuID查询商品SKU信息包含图片列表 解决缓存击穿问题，缓存雪崩设置基础时间+随机时间
     * 方式二：采用Redisson分布式锁进行优化
     *
     * @param skuId
     * @return
     */
    @Override
    public SkuInfo getSkuInfo(Long skuId) {
        try {
            //1.优先从分布式缓存Redis中获取业务数据-String类型
            //1.1 构建业务数据Key 形式（sku:skuId:info）
            String datakey = RedisConst.SKUKEY_PREFIX + skuId + RedisConst.SKUKEY_SUFFIX;

            //1.2 获取业务数据
            SkuInfo skuInfo = (SkuInfo) redisTemplate.opsForValue().get(datakey);
            if (skuInfo != null) {
                return skuInfo;
            }

            //2.尝试获取分布式锁
            //2.1 构建业务数据对应锁的key 粒度小 提升分布式锁性能
            String lockKey = RedisConst.SKUKEY_PREFIX + skuId + RedisConst.SKULOCK_SUFFIX;

            //2.3 创建Redisson提供锁对象 锁：hash
            RLock lock = redissonClient.getLock(lockKey);

            //2.4 尝试获取锁 如果在等待时间内获取锁成功返回true,超时为获取到锁返回false  leaseTime设置为-1开启看门狗
            lock.lock();
            //boolean flag = lock.tryLock(RedisConst.SKULOCK_EXPIRE_PX1, -1, TimeUnit.SECONDS);

            //3.获取分布式锁成功：执行业务
            //if (flag) {
            try {
                //3.1 避免获取分布所失败且处于等待状态线程获取到分布式锁后再次执行查数据库代码，故加一个策略：当获取到分布式锁后，再查询一次缓存
                skuInfo = (SkuInfo) redisTemplate.opsForValue().get(datakey);
                if (skuInfo != null) {
                    return skuInfo;
                }
                //3.2 如果再次查询缓存为空，执行查询数据库逻辑
                skuInfo = this.getSkuInfoFromDB(skuId);
                //3.2.1 如果查询到业务数据为空，存放短暂时间
                if (skuInfo == null) {
                    redisTemplate.opsForValue().set(datakey, skuInfo, RedisConst.SKUKEY_TEMPORARY_TIMEOUT, TimeUnit.SECONDS);
                    return skuInfo;
                } else {
                    //3.2.2 如果查询到业务数据有值，存放较长时间-基础时间+随机时间避免缓存雪崩
                    int randomTimeSec = new Random().nextInt(1000);
                    redisTemplate.opsForValue().set(datakey, skuInfo, RedisConst.SKUKEY_TEMPORARY_TIMEOUT + randomTimeSec, TimeUnit.SECONDS);
                    return skuInfo;
                }
            } finally {
                //4.释放锁
                lock.unlock();
            }
            //} else {
            //    //5.获取分布式锁失败：自旋重试  业务要求必须响应业务数据
            //    Thread.sleep(500);
            //    return this.getSkuInfo(skuId);
            //}
        } catch (Exception e) {
            log.error("[商品服务]查询商品信息异常：{}", e);
            //6.兜底处理方案：查询数据库获取
            return this.getSkuInfoFromDB(skuId);
        }
    }


    /**
     * 根据SkuID查询商品SKU信息包含图片列表 解决缓存击穿问题
     * 方式一：采用SpringDataRedis分布式锁进行优化
     *
     * @param skuId
     * @return
     */
    /*@Override
    public SkuInfo getSkuInfo(Long skuId) {
        try {
            //1.优先从分布式缓存Redis中获取业务数据-String数据类型
            //1.1 构建业务数据key 形式(sku:商品skuId:info)
            String dataKey = RedisConst.SKUKEY_PREFIX + skuId + RedisConst.SKUKEY_SUFFIX;

            //1.2 从缓存中获取数据
            SkuInfo skuInfo = (SkuInfo) redisTemplate.opsForValue().get(dataKey);
            if (skuInfo != null) {
                return skuInfo;
            }

            //2.尝试获取分布式锁
            //2.1 构建业务数据Key 形式（sku:商品SkuId:lock）
            String lockKey = RedisConst.SKUKEY_PREFIX + skuId + RedisConst.SKULOCK_SUFFIX;
            //2.2 为了避免线程间互相释放锁 为当前线程生成唯一编号，将来释放锁判断
            String lockValue = UUID.randomUUID().toString().replaceAll("-", "");

            //2.3 调用set命令配合ex,nx参数获取锁锁
            Boolean flag = redisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, 5, TimeUnit.SECONDS);

            //获取锁成功
            if (flag) {
                try {
                    //3.执行查询数据库业务
                    //3.1 调用方法查询数据库
                    skuInfo = this.getSkuInfoFromDB(skuId);
                    if (skuInfo == null) {
                        //3.2 DB中业务数据不存在：缓存10分钟
                        redisTemplate.opsForValue().set(dataKey, skuInfo, RedisConst.SKUKEY_TEMPORARY_TIMEOUT, TimeUnit.SECONDS);
                        return skuInfo;
                    } else {
                        //3.3 将查询到数据放入缓存 业务数据存在：缓存业务时间30分钟
                        redisTemplate.opsForValue().set(dataKey, skuInfo, RedisConst.SKUKEY_TIMEOUT, TimeUnit.SECONDS);
                        return skuInfo;
                    }
                } finally {
                    //4.执行完业务后将锁释放-判断锁是否为当前线程
                    String scriptText = "if redis.call(\"get\",KEYS[1]) == ARGV[1]\n" +
                            "then\n" +
                            "    return redis.call(\"del\",KEYS[1])\n" +
                            "else\n" +
                            "    return 0\n" +
                            "end";
                    //1.创建封装lua脚本对象
                    DefaultRedisScript<Long> script = new DefaultRedisScript<>();
                    script.setScriptText(scriptText);
                    script.setResultType(Long.class);
                    //2.设置键跟入参的值
                    //3.执行脚本
                    redisTemplate.execute(script, Arrays.asList(lockKey), lockValue);
                }
            } else {
                //5.获取锁失败自旋尝试再次获取-业务要求查询业务数据必须返回
                Thread.sleep(500);
                return this.getSkuInfo(skuId);
            }
        } catch (Exception e) {
            log.error("[商品服务]查询商品信息异常：{}", e);
            return this.getSkuInfoFromDB(skuId);
        }
    }*/


    /**
     * 从数据库中获取商品信息
     *
     * @param skuId
     * @return
     */
    @Override
    @GmallCache(prefix = RedisConst.SKUKEY_PREFIX)
    public SkuInfo getSkuInfoFromDB(Long skuId) {
        //1.根据商品SKU主键查询商品SKU基本信息
        SkuInfo skuInfo = skuInfoService.getById(skuId);

        //2.根据SKUID查询商品图片列表
        if (skuInfo != null) {
            LambdaQueryWrapper<SkuImage> queryWrapper = new LambdaQueryWrapper<>();
            queryWrapper.eq(SkuImage::getSkuId, skuId);
            List<SkuImage> skuImageList = skuImageService.list(queryWrapper);
            skuInfo.setSkuImageList(skuImageList);
            return skuInfo;
        }
        return null;
    }

    /**
     * 获取SKU商品实时价格
     *
     * @param skuId
     * @return
     */
    @Override
    public BigDecimal getSkuPrice(Long skuId) {
        try {
            //1.构建锁key
            String lockKey = RedisConst.SKUKEY_PREFIX + skuId + ":priceLock";
            //2.创建锁对象
            RLock lock = redissonClient.getLock(lockKey);
            //3.获取分布式锁
            boolean flag = lock.tryLock(RedisConst.SKULOCK_EXPIRE_PX1, -1, TimeUnit.SECONDS);
            if (flag) {
                try {
                    //4.获取锁成功执行业务
                    LambdaQueryWrapper<SkuInfo> queryWrapper = new LambdaQueryWrapper<>();
                    queryWrapper.eq(SkuInfo::getId, skuId);
                    queryWrapper.select(SkuInfo::getPrice);
                    SkuInfo skuInfo = skuInfoService.getOne(queryWrapper);
                    if (skuInfo != null) {
                        return skuInfo.getPrice();
                    }
                    return new BigDecimal("0");
                } finally {
                    //5.释放锁
                    lock.unlock();
                }
            } else {
                //6.获取锁失败自旋，尝试再次获取
                Thread.sleep(500);
                this.getSkuPrice(skuId);
            }
        } catch (Exception e) {
            log.error("[商品服务]查询商品：{}，价格异常", skuId);
        }
        return new BigDecimal("0");
    }

    /**
     * 根据skuId查询当前商品的平台属性列表-包含平台属性值
     *
     * @param skuId
     * @return
     */
    @Override
    @GmallCache(prefix = "attrList:")
    public List<BaseAttrInfo> getAttrListBySkuId(Long skuId) {
        SkuAttrValueMapper skuAttrValueMapper = (SkuAttrValueMapper) skuAttrValueService.getBaseMapper();
        return skuAttrValueMapper.getAttrListBySkuId(skuId);
    }

    /**
     * 切换SKU完成跳转“兄弟sku”页面json字符串
     *
     * @param spuId
     * @return
     */
    @Override
    @GmallCache(prefix = "changeSkuValueIds:")
    public String getChangeSkuValueIdsMap(Long spuId) {
        //1.调用持久层方法得到 一组销售属性 跟 SKUID 映射多条记录
        SkuSaleAttrValueMapper skuSaleAttrValueMapper = (SkuSaleAttrValueMapper) skuSaleAttrValueService.getBaseMapper();
        List<Map> skuValueMapList = skuSaleAttrValueMapper.getChangeSkuValueIdsMap(spuId);
        //2.遍历列表转为JSON字符串 "{"3733|3734":24,"3733|3735":25,"3732|3734":26,"3732|3735":27}"
        Map<Object, Object> mapResult = new HashMap<>();
        if (!CollectionUtils.isEmpty(skuValueMapList)) {
            for (Map map : skuValueMapList) {
                Object valueIds = map.get("value_ids");
                Object skuId = map.get("sku_id");
                mapResult.put(valueIds, skuId);
            }
        }
        return JSON.toJSONString(mapResult);
    }
}
